# Copyright 2022 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""WORK IN PROGRESS!

This is intended to be a replacement for the proto codegen in proto.bzl, which
relies on the transitive proto compilation support removed from newer versions
of rules_proto_grpc. However, the version checked in here does not yet support,

1. Proto libraries with dependencies in external repositories.
2. Proto libraries with strip_import_prefix or import_prefix attributes.

In addition, nanopb proto files are not yet generated.

TODO(b/234873954): Close these gaps and start using this implementation.

# Overview of implementation

(If you just want to use the macros, see their docstrings; this section is
intended to orient future maintainers.)

Proto code generation is carried out by the _pw_proto_library,
_pw_raw_rpc_proto_library and _pw_nanopb_rpc_proto_library rules using aspects
(https://docs.bazel.build/versions/main/skylark/aspects.html). A
_pw_proto_library has a single proto_library as a dependency, but that
proto_library may depend on other proto_library targets; as a result, the
generated .pwpb.h file #include's .pwpb.h files generated from the dependency
proto_libraries. The aspect propagates along the proto_library dependency
graph, running the proto compiler on each proto_library in the original
target's transitive dependencies, ensuring that we're not missing any .pwpb.h
files at C++ compile time.

Although we have a separate rule for each protocol compiler plugin
(_pw_proto_library, _pw_raw_rpc_proto_library, _pw_nanopb_rpc_proto_library),
they actually share an implementation (_pw _impl_pw_proto_library) and use
similar aspects, all generated by _proto_compiler_aspect.
"""

load("//pw_build:pigweed.bzl", "pw_cc_library")
load("@rules_proto//proto:defs.bzl", "ProtoInfo")
load("//pw_protobuf_compiler:pw_nanopb_cc_library.bzl", "pw_nanopb_cc_library")

def pwpb_proto_library(name, deps, tags = None, visibility = None):
    """A C++ proto library generated using pw_protobuf.

    Attributes:
      deps: proto_library targets for which to generate this library.
    """
    name_pb = name + ".pb"

    _pw_proto_library(
        name = name_pb,
        deps = deps,
    )

    pw_cc_library(
        name = name,
        hdrs = [":" + name_pb],
        deps = [
            "//pw_assert:facade",
            "//pw_containers:vector",
            "//pw_preprocessor",
            "//pw_protobuf",
            "//pw_result",
            "//pw_span",
            "//pw_status",
            "//pw_string:string",
        ],
        linkstatic = 1,
        tags = tags,
        visibility = visibility,
    )

def pwpb_rpc_proto_library(name, deps, pwpb_proto_library_deps, tags = None, visibility = None):
    """A pwpb_rpc proto library target.

    Attributes:
      deps: proto_library targets for which to generate this library.
      pwpb_proto_library_deps: A pwpb_proto_library generated
        from the same proto_library. Required.
    """
    name_pb = name + ".pb"

    _pw_pwpb_rpc_proto_library(
        name = name_pb,
        deps = deps,
    )

    pw_cc_library(
        name = name,
        hdrs = [":" + name_pb],
        deps = [
            "//pw_protobuf",
            "//pw_rpc",
            "//pw_rpc/pwpb:client_api",
            "//pw_rpc/pwpb:server_api",
        ] + pwpb_proto_library_deps,
        linkstatic = 1,
        tags = tags,
        visibility = visibility,
    )

def raw_rpc_proto_library(name, deps, tags = None, visibility = None):
    """A raw C++ RPC proto library."""
    name_pb = name + ".pb"

    _pw_raw_rpc_proto_library(
        name = name_pb,
        deps = deps,
    )

    pw_cc_library(
        name = name,
        hdrs = [":" + name_pb],
        deps = [
            "//pw_rpc",
            "//pw_rpc/raw:client_api",
            "//pw_rpc/raw:server_api",
        ],
        linkstatic = 1,
        tags = tags,
        visibility = visibility,
    )

def nanopb_rpc_proto_library(name, deps, nanopb_proto_library_deps, tags = None, visibility = None):
    """A C++ RPC proto library using nanopb.

    Attributes:
      deps: proto_library targets for which to generate this library.
      nanopb_proto_library_deps: A pw_nanopb_cc_library generated
        from the same proto_library. Required.
    """
    name_pb = name + ".pb"

    _pw_nanopb_rpc_proto_library(
        name = name_pb,
        deps = deps,
    )

    pw_cc_library(
        name = name,
        hdrs = [":" + name_pb],
        deps = [
            "//pw_rpc",
            "//pw_rpc/nanopb:client_api",
            "//pw_rpc/nanopb:server_api",
        ] + nanopb_proto_library_deps,
        linkstatic = 1,
        tags = tags,
        visibility = visibility,
    )

def pw_proto_library(
        name,
        deps,
        visibility = None,
        tags = None,
        nanopb_options = None,
        enabled_targets = None):
    """Generate Pigweed proto C++ code.

    DEPRECATED. This macro is deprecated and will be removed in a future
    Pigweed version. Please use the single-target macros above.

    Args:
      name: The name of the target.
      deps: proto_library targets from which to generate Pigweed C++.
      visibility: The visibility of the target. See
         https://bazel.build/concepts/visibility.
      tags: Tags for the target. See
         https://bazel.build/reference/be/common-definitions#common-attributes.
      nanopb_options: path to file containing nanopb options, if any
        (https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options).
      enabled_targets: Specifies which libraries should be generated. Libraries
        will only be generated as needed, but unnecessary outputs may conflict
        with other build rules and thus cause build failures. This filter allows
        manual selection of which libraries should be supported by this build
        target in order to prevent such conflicts. The argument, if provided,
        should be a subset of ["pwpb", "nanopb", "raw_rpc", "nanopb_rpc"]. All
        are enabled by default. Note that "nanopb_rpc" relies on "nanopb".

    Example usage:

      proto_library(
        name = "benchmark_proto",
        srcs = [
          "benchmark.proto",
        ],
      )

      pw_proto_library(
        name = "benchmark_pw_proto",
        deps = [":benchmark_proto"],
      )

      pw_cc_binary(
        name = "proto_user",
        srcs = ["proto_user.cc"],
        deps = [":benchmark_pw_proto.pwpb"],
      )

    The pw_proto_library generates the following targets in this example:

    "benchmark_pw_proto.pwpb": C++ library exposing the "benchmark.pwpb.h" header.
    "benchmark_pw_proto.pwpb_rpc": C++ library exposing the
        "benchmark.rpc.pwpb.h" header.
    "benchmark_pw_proto.raw_rpc": C++ library exposing the "benchmark.raw_rpc.h"
        header.
    "benchmark_pw_proto.nanopb": C++ library exposing the "benchmark.pb.h"
        header.
    "benchmark_pw_proto.nanopb_rpc": C++ library exposing the
        "benchmark.rpc.pb.h" header.
    """

    def is_plugin_enabled(plugin):
        return (enabled_targets == None or plugin in enabled_targets)

    if is_plugin_enabled("nanopb"):
        # Use nanopb to generate the pb.h and pb.c files, and the target
        # exposing them.
        pw_nanopb_cc_library(
            name = name + ".nanopb",
            deps = deps,
            visibility = visibility,
            tags = tags,
            options = nanopb_options,
        )

    if is_plugin_enabled("pwpb"):
        pwpb_proto_library(
            name = name + ".pwpb",
            deps = deps,
            tags = tags,
            visibility = visibility,
        )

    if is_plugin_enabled("pwpb_rpc"):
        pwpb_rpc_proto_library(
            name = name + ".pwpb_rpc",
            deps = deps,
            pwpb_proto_library_deps = [":" + name + ".pwpb"],
            tags = tags,
            visibility = visibility,
        )

    if is_plugin_enabled("raw_rpc"):
        raw_rpc_proto_library(
            name = name + ".raw_rpc",
            deps = deps,
            tags = tags,
            visibility = visibility,
        )

    if is_plugin_enabled("nanopb_rpc"):
        nanopb_rpc_proto_library(
            name = name + ".nanopb_rpc",
            deps = deps,
            nanopb_proto_library_deps = [":" + name + ".nanopb"],
            tags = tags,
            visibility = visibility,
        )

PwProtoInfo = provider(
    "Returned by PW proto compilation aspect",
    fields = {
        "genfiles": "generated C++ header files",
    },
)

PwProtoOptionsInfo = provider(
    "Allows `pw_proto_filegroup` targets to pass along `.options` files " +
    "without polluting the `DefaultInfo` provider, which means they can " +
    "still be used in the `srcs` of `proto_library` targets.",
    fields = {
        "options_files": (".options file(s) associated with a proto_library " +
                          "for Pigweed codegen."),
    },
)

def _get_short_path(source):
    return source.short_path

def _get_path(file):
    return file.path

def _proto_compiler_aspect_impl(target, ctx):
    # List the files we will generate for this proto_library target.
    genfiles = []

    for src in target[ProtoInfo].direct_sources:
        path = src.basename[:-len("proto")] + ctx.attr._extension
        genfiles.append(ctx.actions.declare_file(path, sibling = src))

    # List the `.options` files from any `pw_proto_filegroup` targets listed
    # under this target's `srcs`.
    options_files = [
        options_file
        for src in ctx.rule.attr.srcs
        if PwProtoOptionsInfo in src
        for options_file in src[PwProtoOptionsInfo].options_files.to_list()
    ]

    # Convert include paths to a depset and back to deduplicate entries.
    # Note that this will probably evaluate to either [] or ["."] in most cases.
    options_file_include_paths = depset([
        "." if options_file.root.path == "" else options_file.root.path
        for options_file in options_files
    ]).to_list()

    args = ctx.actions.args()
    args.add("--plugin=protoc-gen-pwpb={}".format(ctx.executable._protoc_plugin.path))
    for options_file_include_path in options_file_include_paths:
        args.add("--pwpb_opt=-I{}".format(options_file_include_path))
    args.add("--pwpb_opt=--no-legacy-namespace")
    args.add("--pwpb_out={}".format(ctx.bin_dir.path))
    args.add_joined(
        "--descriptor_set_in",
        target[ProtoInfo].transitive_descriptor_sets,
        join_with = ctx.configuration.host_path_separator,
        map_each = _get_path,
    )

    args.add_all(target[ProtoInfo].direct_sources, map_each = _get_short_path)

    ctx.actions.run(
        inputs = depset(
            target[ProtoInfo].transitive_sources.to_list() + options_files,
            transitive = [target[ProtoInfo].transitive_descriptor_sets],
        ),
        progress_message = "Generating %s C++ files for %s" % (ctx.attr._extension, ctx.label.name),
        tools = [ctx.executable._protoc_plugin],
        outputs = genfiles,
        executable = ctx.executable._protoc,
        arguments = [args],
    )

    transitive_genfiles = genfiles
    for dep in ctx.rule.attr.deps:
        transitive_genfiles += dep[PwProtoInfo].genfiles
    return [PwProtoInfo(genfiles = transitive_genfiles)]

def _proto_compiler_aspect(extension, protoc_plugin):
    """Returns an aspect that runs the proto compiler.

    The aspect propagates through the deps of proto_library targets, running
    the proto compiler with the specified plugin for each of their source
    files. The proto compiler is assumed to produce one output file per input
    .proto file. That file is placed under bazel-bin at the same path as the
    input file, but with the specified extension (i.e., with _extension =
    .pwpb.h, the aspect converts pw_log/log.proto into
    bazel-bin/pw_log/log.pwpb.h).

    The aspect returns a provider exposing all the File objects generated from
    the dependency graph.
    """
    return aspect(
        attr_aspects = ["deps"],
        attrs = {
            "_extension": attr.string(default = extension),
            "_protoc": attr.label(
                default = Label("@com_google_protobuf//:protoc"),
                executable = True,
                cfg = "exec",
            ),
            "_protoc_plugin": attr.label(
                default = Label(protoc_plugin),
                executable = True,
                cfg = "exec",
            ),
        },
        implementation = _proto_compiler_aspect_impl,
        provides = [PwProtoInfo],
    )

def _impl_pw_proto_library(ctx):
    """Implementation of the proto codegen rule.

    The work of actually generating the code is done by the aspect, so here we
    just gather up all the generated files and return them.
    """

    # Note that we don't distinguish between the files generated from the
    # target, and the files generated from its dependencies. We return all of
    # them together, and in pw_proto_library expose all of them as hdrs.
    # Pigweed's plugins happen to only generate .h files, so this works, but
    # strictly speaking we should expose only the files generated from the
    # target itself in hdrs, and place the headers generated from dependencies
    # in srcs. We don't perform layering_check in Pigweed, so this is not a big
    # deal.
    #
    # TODO(b/234873954): Tidy this up.
    all_genfiles = []
    for dep in ctx.attr.deps:
        for f in dep[PwProtoInfo].genfiles:
            all_genfiles.append(f)

    return [DefaultInfo(files = depset(all_genfiles))]

# Instantiate the aspects and rules for generating code using specific plugins.
_pw_proto_compiler_aspect = _proto_compiler_aspect("pwpb.h", "//pw_protobuf/py:plugin")

_pw_proto_library = rule(
    implementation = _impl_pw_proto_library,
    attrs = {
        "deps": attr.label_list(
            providers = [ProtoInfo],
            aspects = [_pw_proto_compiler_aspect],
        ),
    },
)

_pw_pwpb_rpc_proto_compiler_aspect = _proto_compiler_aspect("rpc.pwpb.h", "//pw_rpc/py:plugin_pwpb")

_pw_pwpb_rpc_proto_library = rule(
    implementation = _impl_pw_proto_library,
    attrs = {
        "deps": attr.label_list(
            providers = [ProtoInfo],
            aspects = [_pw_pwpb_rpc_proto_compiler_aspect],
        ),
    },
)

_pw_raw_rpc_proto_compiler_aspect = _proto_compiler_aspect("raw_rpc.pb.h", "//pw_rpc/py:plugin_raw")

_pw_raw_rpc_proto_library = rule(
    implementation = _impl_pw_proto_library,
    attrs = {
        "deps": attr.label_list(
            providers = [ProtoInfo],
            aspects = [_pw_raw_rpc_proto_compiler_aspect],
        ),
    },
)

_pw_nanopb_rpc_proto_compiler_aspect = _proto_compiler_aspect("rpc.pb.h", "//pw_rpc/py:plugin_nanopb")

_pw_nanopb_rpc_proto_library = rule(
    implementation = _impl_pw_proto_library,
    attrs = {
        "deps": attr.label_list(
            providers = [ProtoInfo],
            aspects = [_pw_nanopb_rpc_proto_compiler_aspect],
        ),
    },
)

def _pw_proto_filegroup_impl(ctx):
    source_files = list()
    options_files = list()

    for src in ctx.attr.srcs:
        source_files += src.files.to_list()

    for options_src in ctx.attr.options_files:
        for file in options_src.files.to_list():
            if file.extension == "options":
                options_files.append(file)
            else:
                fail((
                    "Files provided as `options_files` to a " +
                    "`pw_proto_filegroup` must have the `.options` " +
                    "extension; the file `{}` was provided."
                ).format(file.basename))

    return [
        DefaultInfo(files = depset(source_files)),
        PwProtoOptionsInfo(options_files = depset(options_files)),
    ]

pw_proto_filegroup = rule(
    doc = (
        "Acts like a `filegroup`, but with an additional `options_files` " +
        "attribute that accepts a list of `.options` files. These `.options` " +
        "files should typically correspond to `.proto` files provided under " +
        "the `srcs` attribute." +
        "\n\n" +
        "A `pw_proto_filegroup` is intended to be passed into the `srcs` of " +
        "a `proto_library` target as if it were a normal `filegroup` " +
        "containing only `.proto` files. For the purposes of the " +
        "`proto_library` itself, the `pw_proto_filegroup` does indeed act " +
        "just like a normal `filegroup`; the `options_files` attribute is " +
        "ignored. However, if that `proto_library` target is then passed " +
        "(directly or transitively) into the `deps` of a `pw_proto_library` " +
        "for code generation, the `pw_proto_library` target will have access " +
        "to the provided `.options` files and will pass them to the code " +
        "generator." +
        "\n\n" +
        "Note that, in order for a `pw_proto_filegroup` to be a valid `srcs` " +
        "entry for a `proto_library`, it must meet the same conditions " +
        "required of a standard `filegroup` in that context. Namely, its " +
        "`srcs` must provide at least one `.proto` (or `.protodevel`) file. " +
        "Put simply, a `pw_proto_filegroup` cannot be used as a vector for " +
        "injecting solely `.options` files; it must contain at least one " +
        "proto as well (generally one associated with an included `.options` " +
        "file in the interest of clarity)." +
        "\n\n" +
        "Regarding the somewhat unusual usage, this feature's design was " +
        "mostly preordained by the combination of Bazel's strict access " +
        "controls, the restrictions imposed on inputs to the `proto_library` " +
        "rule, and the need to support `.options` files from transitive " +
        "dependencies."
    ),
    implementation = _pw_proto_filegroup_impl,
    attrs = {
        "srcs": attr.label_list(
            allow_files = True,
        ),
        "options_files": attr.label_list(
            allow_files = True,
        ),
    },
    provides = [PwProtoOptionsInfo],
)
